﻿namespace FsTestProject

open Microsoft.FSharp
open Microsoft.FSharp.Collections

type public Polynomial(values) =

    let values = values

    //#region ToString components

    let getDoubleString (value:double) =
        let rnd = System.Math.Round(value, 0)
        if rnd = value
        then sprintf "%d" (int value)
        else sprintf "%f" value

    let getCoefficientString coefficient isSignRequired isValueRequired =
        match coefficient, isSignRequired, isValueRequired with
        | 1.0, true, true -> Some("+ 1"), true
        | 1.0, true, false -> Some("+"), false
        | 1.0, false, true -> Some("1"), true
        | 1.0, false, false -> None, false
        | -1.0, true, true -> Some("- 1"), true
        | -1.0, false, true -> Some("-1"), false
        | -1.0, _, false -> Some("-"), false
        | _, false, _ -> Some(string coefficient), true
        | _, true, _ -> if coefficient < 0.0
                        then
                            let abs = coefficient * -1.0
                            Some(sprintf "- %s" (getDoubleString abs)), true
                        else
                            Some(sprintf "+ %s" (getDoubleString coefficient)), true

    let getXString power = 
        match power with
        | 0 -> None
        | 1 -> Some("x")
        | _  -> Some(sprintf "x^%d" power)

    let getTermString coefficient power isSignRequired = 
        match coefficient with
        | 0.0 -> None
        | _ -> 
            let coefficientString, multiply = getCoefficientString coefficient isSignRequired (power = 0)
            let xString = getXString power
            match coefficientString, multiply, xString with
            | None, _, None -> None
            | None, _, Some(x) -> Some(x)
            | Some(m), _, None -> Some(m)
            | Some(m), false, Some(x) -> if isSignRequired
                                         then Some(String.concat " " [m;x])
                                         else Some(String.concat "" [m;x])
            | Some(m), true, Some(x) -> Some(String.concat " * " [m;x])

    let rec getValuesString acc values power isSignRequired =
        match values with
        | [] -> acc
        | head::tail ->
            let current = getTermString head power isSignRequired
            match current with
            | None -> getValuesString acc tail (power + 1) isSignRequired
            | Some(x) -> getValuesString (String.concat " " [acc;x]) tail (power + 1) true

    let getString values = 
        getValuesString "y =" values 0 false

    //#endregion

    //#region Parse components

    static let parseDoubleInvariant i = 
        System.Double.Parse(i, System.Globalization.NumberFormatInfo.InvariantInfo)

    static let unsafePeekChar (input:string) index =
        input.[index]

    static let peekChar input index = 
        match index with
        | _ when index >= (String.length input) -> None
        | _ -> Some(unsafePeekChar input index)

    static let skipChars condition input index  =
        let size = (String.length input) - 1
        [index..size]
        |> List.tryFind (fun i -> not(condition(unsafePeekChar input i)))

    static let skipWhiteSpaces = skipChars System.Char.IsWhiteSpace

    static let skipWhiteSpacesAndStars = skipChars (fun c -> System.Char.IsWhiteSpace(c) || c = '*')

    static let skipDigitsAndOtherDoubleRelatedStuff = skipChars (fun c-> System.Char.IsDigit(c) || c = 'e' || c = 'E' || c = '-' || c = '.')

    static let skipYandEquals input index =
        let afterY = skipChars (fun c -> c = 'y') input index
        match afterY with
        | None -> invalidArg "input" "incorrect input"
        | Some(i) -> let afterWhiteSpaces = skipWhiteSpaces input i
                     match afterWhiteSpaces with
                     | None -> invalidArg "input" "incorrect input"
                     | Some(i) -> let afterEquals = skipChars (fun c -> c = '=') input i
                                  match afterEquals with
                                  | None -> invalidArg "input" "incorrect input"
                                  | Some(i) -> i

    static let getSign input index =
        let afterWhitespaces = skipWhiteSpaces input index
        match afterWhitespaces with
        | None -> None, -1
        | Some(x) -> let next = peekChar input x
                     match next with
                     | Some('-') -> Some(-1.0), (x + 1)
                     | Some('+') -> Some(1.0), (x + 1)
                     | None | Some('x') -> Some(1.0), x
                     | Some(c) when (System.Char.IsDigit c) -> Some(1.0), x
                     | _ -> invalidArg "input" "incorrect input"

    static let getNumber input index =
        let afterWhitespaces = skipWhiteSpaces input index
        match afterWhitespaces with
        | None -> None, -1
        | Some(x) -> let next = skipDigitsAndOtherDoubleRelatedStuff input x
                     match next with
                     | None -> Some(parseDoubleInvariant input.[x..]), (String.length input)
                     | Some(y) when x = y -> Some(1.0), x
                     | Some(y) -> Some(parseDoubleInvariant input.[x..(y-1)]), y

    static let getPower input index =
        let afterWhitespacesAndStar = skipWhiteSpacesAndStars input index
        match afterWhitespacesAndStar with
        | None -> None, -1
        | Some(x) -> let next = peekChar input x
                     match next with
                     | Some('x') -> let yetNext = peekChar input (x + 1)
                                    match yetNext with
                                    | Some('^') -> let power = getNumber input (x + 2)
                                                   match power with
                                                   | None, _ -> None, -1
                                                   | Some(y), i -> Some(y), i
                                    | _ -> Some(1.0), (x + 1)
                     | _ -> Some(0.0), x

    static let getTerm input index =
        let s = getSign input index
        match s with
        | None, _ -> None, -1
        | Some(sValue), i -> let c = getNumber input i
                             match c with
                             | None, _ -> None, -1
                             | Some(cValue), i -> let p = getPower input i
                                                  match p with
                                                  | None, _ -> None, -1
                                                  | Some(pValue), i -> Some(sValue * cValue, pValue), i

    static let pushValue acc coefficient power =
        List.concat [acc;(List.replicate (power - (List.length acc)) 0.0);[coefficient]]

    static let rec getValues acc input index =
        let term = getTerm input index
        match term with 
        | None, _ -> acc
        | Some(c, p), i -> getValues (pushValue acc c (int p)) input i

    static let get input =
        getValues [] input (skipYandEquals input 0)

    //#endregion

    member this.Values = values

    static member Parse(input) = 
        new Polynomial(get input)

    member this.CalcY(x) = 
        values 
        |> List.mapi (fun i m -> (m, i)) 
        |> List.sumBy (fun z -> fst z * pown x (snd z))

    override this.ToString() = getString values


        

